/*
 * Copyright (c) 2010-2011 Josef Hardi <josef.hardi@gmail.com>
 * Copyright (c) 2010 Piotr Zagawa <piotr.zagawa@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package id.duapuluhdua.dakidaki.gps;

import java.util.Observable;

import android.content.Context;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.widget.Toast;

import id.duapuluhdua.dakidaki.Preferences;
import id.duapuluhdua.dakidaki.R;

/**
 *
 */
public abstract class GpsReader extends Observable
{
  protected static final String TAG = "GpsReader";

  /** Constants */
  public final static float MAXMIMUM_ACCURACY = 5.0f;
  public final static float MINIMUM_ACCURACY = 200.0f;
  public final static Integer LOCATION_READY = 1;

  /** Fields */
  private static String providerName = LocationManager.GPS_PROVIDER;
  private String mServiceNotEnabledMsg;

  private GpsStatus mGpsStatus;
  private LocationManager mLocManager;
  private LocationProvider mLocProvider;

  protected Context mContext;
  protected Preferences mPreferences;
  protected Vibrator mVibrator;

  protected Boolean bStateMutex = false;
  protected Boolean bStarted = false;

  /**
   * The default constructor.
   *
   * @param context
   *            The application context.
   * @param prefs
   *            The application preference.
   */
  public GpsReader(Context context, Preferences prefs)
  {
    mContext = context;
    mPreferences = prefs;

    mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
    mLocManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
    mServiceNotEnabledMsg = context.getString(R.string.notify_msg_loc_service_disabled);

    getLocationProvider();
  }

  /**
   * Returns the current enable/disable status of the GPS provider. If the
   * user has enabled this provider in the Settings menu, true is returned
   * otherwise false is returned.
   *
   * @return true if the provider is enabled.
   */
  public boolean isEnabled()
  {
    if (mLocProvider == null) {
      return false;
    }
    if (providerName == null) {
      return false;
    }
    return mLocManager.isProviderEnabled(providerName);
  }

  /**
   * Returns the current start/stop status of the GPS service. If the user
   * has launched the service, true is returned otherwise false is returned.
   *
   * @return true if the service is started.
   */
  public boolean isStarted()
  {
    synchronized (bStateMutex)
    {
      return bStarted;
    }
  }

  /**
   * Checks whether the location provider has been initialized or not.
   *
   * @return true if the provider is not null.
   */
  public boolean isLocationProvider()
  {
    return (mLocProvider != null);
  }

  /** Get location provider object */
  private LocationProvider getLocationProvider()
  {
    mLocProvider = mLocManager.getProvider(providerName);

    return mLocProvider;
  }

  /**
   * Returns the accuracy tolerance. If the accuracy is lower than the
   * minimum threshold, true is returned otherwise false is returned.
   *
   * @param location the geographical location sensed by the GPS sensor.
   *
   * @return true if the accuracy is acceptable.
   */
  private boolean isAccuracyAcceptable(Location location)
  {
    if (location.hasAccuracy()) {
      return (location.getAccuracy() < MINIMUM_ACCURACY) ? true : false;
    }
    return false;
  }

  /**
   * Starts the GPS locating service. The users may decide how many seconds
   * for the service updating the new location.
   *
   * @param locationUpdateInterval
   *            The refresh time between updates (in second).
   */
  protected void start(int locationUpdateInterval)
  {
    if (isStarted()) {  // Do nothing if it is already started.
      return;
    }
    if (!isLocationProvider()) {
      getLocationProvider();
    }

    mPreferences.load();

    if (isEnabled()) {
      synchronized (bStateMutex)
      {
        bStarted = false;
        try {
          final long interval = locationUpdateInterval * 1000;
          mLocManager.requestLocationUpdates(providerName, interval, 0, eventLocation);
          mLocManager.addGpsStatusListener(eventGpsStatus);
          bStarted = true;
        }
        catch (Exception e) {
          Log.e(TAG, e.toString());
        }
      }
    }
    else {
      Toast.makeText(mContext, mServiceNotEnabledMsg, Toast.LENGTH_LONG).show();
    }
  }

  /**
   * Halts the service.
   */
  public void stop()
  {
    if (isStarted()) {
      synchronized (bStateMutex)
      {
        mLocManager.removeUpdates(eventLocation);
        mLocManager.removeGpsStatusListener(eventGpsStatus);
        bStarted = false;
      }
    }
  }

  /**
   * Halts the service by force.
   */
  public void forceStop()
  {
    synchronized (bStateMutex)
    {
      try {
        mLocManager.removeUpdates(eventLocation);
        mLocManager.removeGpsStatusListener(eventGpsStatus);
      }
      catch (Exception e) {
        Log.e(TAG, e.toString());
      }
      bStarted = false;
    }
  }

  /**
   * Returns the last geographical location from the sensor reading.
   *
   * @return the last location.
   */
  public Location getLastLocation()
  {
    if (isLocationProvider()) {
      return mLocManager.getLastKnownLocation(providerName);
    }
    return null;
  }

  /**
   * Instructs the phone's vibrator to vibrate.
   */
  protected void vibrate()
  {
    if (mVibrator != null) {
      mVibrator.vibrate(50);
    }
  }

  /** GPS status listener */
  private GpsStatus.Listener eventGpsStatus = new GpsStatus.Listener() {
    @Override
    public void onGpsStatusChanged(int event)
    {
      if (mGpsStatus == null) {
        mGpsStatus = mLocManager.getGpsStatus(null);
      }
      else {
        mLocManager.getGpsStatus(mGpsStatus);
      }

      if (mGpsStatus != null) {
        if (event == GpsStatus.GPS_EVENT_STOPPED) {
          setChanged();
          notifyObservers(LOCATION_READY);
        }
      }
    }
  };

  /** Location listener */
  private LocationListener eventLocation = new LocationListener() {
    @Override
    public void onLocationChanged(Location location)
    {
      if (location != null) {
        final boolean bAccuracyAcceptable = isAccuracyAcceptable(location);
        onGpsLocationChanged(location, bAccuracyAcceptable);
        if (bAccuracyAcceptable) {
          setChanged();
          notifyObservers(LOCATION_READY);
        }
      }
    }

    @Override
    public void onProviderDisabled(String provider)
    {
      // Do nothing.
    }

    @Override
    public void onProviderEnabled(String provider)
    {
      // Do nothing.
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras)
    {
      switch (status) { // TODO Do something for each status.
        case LocationProvider.OUT_OF_SERVICE:
          Log.i(TAG, "Out of service.");
          break;
        case LocationProvider.TEMPORARILY_UNAVAILABLE:
          Log.i(TAG, "Service is temporarly unavailable.");
          break;
        case LocationProvider.AVAILABLE:
          Log.i(TAG, "Service is available.");
          break;
      }
    }
  };

  // Implement later by the subclass
  protected abstract void onGpsLocationChanged(Location location, boolean accuracyAcceptable);
}
